package com.siebre.picclife.asynctask.springcontext;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ScannedGenericBeanDefinition;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.gateway.GatewayProxyFactoryBean;

import com.google.common.base.Function;
import com.google.common.collect.Maps;
import com.google.common.reflect.Invokable;
import com.siebre.picclife.asynctask.TaskException;
import com.siebre.picclife.asynctask.utils.TaskMappingUtils;

/**
 * Scans task annotations and automatically register proxy beans.
 * 
 * @author ZhangChi
 * @since 2015年4月12日
 *
 */
public class TaskProxyRegistrar implements BeanFactoryPostProcessor {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(TaskProxyRegistrar.class);
	
	private final String packageName;

	private final MethodMetadataFactory methodMetadataFactory;

	public TaskProxyRegistrar(String packageName) {
		this.packageName = packageName;
		methodMetadataFactory = new MethodMetadataFactory();
	}

	public void postProcessBeanFactory(
			ConfigurableListableBeanFactory beanFactory) throws BeansException {
		TaskProxyScanner scanner = new TaskProxyScanner();
		Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(packageName);
		LOGGER.debug("found {} task proxy interfaces", candidateComponents.size());
		for (BeanDefinition bd : candidateComponents) {
			ScannedGenericBeanDefinition sbd = (ScannedGenericBeanDefinition) bd;
			try {
				sbd.resolveBeanClass(getClass().getClassLoader());
				registerProxy(sbd, beanFactory);
			} catch (Exception e) {
				throw new TaskException("failed to register task proxy for " + sbd.getBeanClass(), e);
			}
		}
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void registerProxy(ScannedGenericBeanDefinition sbd, ConfigurableListableBeanFactory beanFactory) throws Exception {
		Class<?> serviceInterface = sbd.getBeanClass();

		GatewayProxyFactoryBean factory = new GatewayProxyFactoryBean(serviceInterface);
		
		MessageChannel requestChannel = beanFactory.getBean("service-args-channel", MessageChannel.class);
		factory.setDefaultRequestChannel(requestChannel);

		Map methodMetadataMap = methodMetadataFactory.create(serviceInterface);
		factory.setMethodMetadataMap(methodMetadataMap);
		
		factory.afterPropertiesSet();

		try {
			Object bean = factory.getObject();
			String beanName = StringUtils.uncapitalize(serviceInterface.getSimpleName()) + "Proxy";
			beanFactory.registerSingleton(beanName, bean);
		} catch (Exception e) {
			throw new TaskException("failed to create gateway for " + serviceInterface, e);
		}
	}
	
	/**
	 * 
	 * @author ZhangChi
	 * @since 2015年4月14日
	 *
	 */
	static class MethodMetadataFactory {
		
		private static final Function<Method, Object> METHOD_META_DATA_FACTORY;
		
		static {
			try {
				Class<?> clazz = Class.forName("org.springframework.integration.gateway.GatewayMethodMetadata");
				final Invokable<?, ?> constructor = Invokable.from(clazz.getDeclaredConstructor());
				constructor.setAccessible(true);
				@SuppressWarnings("unchecked")
				final Invokable<Object, Object> headersSetter = (Invokable<Object, Object>) Invokable.from(clazz.getDeclaredMethod("setHeaderExpressions", Map.class));
				headersSetter.setAccessible(true);
				
				METHOD_META_DATA_FACTORY = new Function<Method, Object>() {

					public Object apply(Method method) {
						Object methodMetadata;
						try {
							methodMetadata = constructor.invoke(null);
							String taskType = TaskMappingUtils.resolveTaskType(method);
							headersSetter.invoke(methodMetadata, Collections.singletonMap("taskType", new LiteralExpression(taskType)));
							return methodMetadata;
						} catch (InvocationTargetException e) {
							throw new TaskException("failed to initialize TaskAnnotationProcessor", e.getCause());
						} catch (Exception e) {
							throw new TaskException("failed to initialize TaskAnnotationProcessor", e);
						}
					}
				};
			} catch (Exception e) {
				throw new TaskException("failed to initialize TaskAnnotationProcessor", e);
			}
		}
		
		Map<String, Object> create(Class<?> serviceInterface) {
			Map<String, Object> methodMetadataMap = Maps.newHashMap();
			
			for (Method method : serviceInterface.getDeclaredMethods()) {
				
				Object methodMetadata = METHOD_META_DATA_FACTORY.apply(method);
				if (methodMetadataMap.containsKey(method.getName())) {
//					Object existed = methodMetadataMap.get(method.getName());
//					if (!existed.equals(methodMetadata)) {
//					}
					throw new TaskException(String.format("method overload is not supported yet, interface=%s, methodName=%s", method.getDeclaringClass().getName(), method.getName()));
				}
				methodMetadataMap.put(method.getName(), methodMetadata);
			}
			
			return methodMetadataMap;
		}
	}
}
